#! /usr/bin/python

__author__="joepearson"
__date__ ="$May 5, 2011 2:56:18 PM$"

from PyQt4 import QtGui, QtCore
import xml.etree.ElementTree as etree
import threading
from OSC import ThreadingOSCServer, OSCMessage
import socket
from AddMIDIForwardWindow import AddMIDIForwardWindow

global debug
debug = False

global version
version = "1.0"

class ServerThread(threading.Thread):
    """
    The thread that will run the server process.
    """
    def __init__(self, ip, port):
        super(ServerThread, self).__init__()


        self.ip = ip
        self.port = port
        self.daemon = True

        self.oscServer = ThreadingOSCServer((ip, port))
        self.oscServer.addMsgHandler('default', self.defaultMessageHandler)

        # Both threads need access to the configuration file.
        # This is a bad way of doing this, but for now, it will work.
        self.configuration = etree.parse("conf/conf.xml")

        

    def run(self):
        """
        The actual worker part of the thread.
        """

        self.oscServer.serve_forever()


    def defaultMessageHandler(self, addr, tags, data, client_address):
            """
            Default handler for the OSCServer.
            """

            #

            print addr,tags, data, client_address

            dealtWith = False

            forwards = self.configuration.findall('forward')

            for rule in forwards:
                # If the address is in the rules list or if it is an automatically
                # mapped pitch.
                if (addr == rule.attrib['address']):
                    dealtWith = True

                    if rule.attrib['destination'] == "midi" or "MIDI":
                        # the rule is a MIDI forwarding rule.
                        channel = int(rule.attrib['channel'])
                        message = rule.attrib['message']
                        try:
                            data1 = rule.attrib['data1']
                            if data1 == "%oscdata":
                                #Replace the "%oscdata" tag with some useful data instead.
                                data1 = float(data[0])*127
                        except KeyError:
                            # Raised when there isn't an attribute of "data1".  This
                            # doesn't really matter so much; when this is passed to MIDI,
                            # the device at the other end will simply assume 0 for data1.
                            pass

                        try:
                            data2 = rule.attrib['data2']
                            if data2 == "%oscdata":
                                #Replace the %oscdata tag with some useful data instead.
                                data2 = float(data[0])*127
                        except KeyError:
                            # Raised when there isn't an attribute of "data1".  This
                            # doesn't really matter so much; when this is passed to MIDI,
                            # the device at the other end will simply assume 0 for data1.
                            pass


                        # NOTE ON OFF

                        if message == "noteOn":
                            # Send a noteOn message.
                            iac.noteOn(channel, int(data1), int(data2))

                        elif message == "ccChange":
                            # Send a CC message.
                            iac.ccChange(channel, int(data1), int(data2))

                        print "%s channel: %i data1: %s data2: %s" %\
                            (message,channel,int(data1),int(data2))

                    elif rule.attrib['destination'][0] == "/":

                        # An OSC rule.

                        # Put together a new OSC Message and attach the
                        # original data.
                        newMessage = OSC.OSCMessage(rule.attrib['destination'])
                        newMessage.append(data)

                        # Get the configuration data and create a tuple of it.
                        host = rule.attrib['hostname']
                        port = rule.attrib['port']
                        conf = (host, int(port))

                        # Send the message
                        self.oscServer.client.sendto(newMessage, conf)


            if not dealtWith:
                # See if it's an automatically mappaple pitch.
                # For example /note/60 maps to midi pitch 60 and so on.

                if addr[0:6] == "/note/":
                    dealtWith = True
                    pitchNumber = addr[6:]

                    channel = int(rule.attrib['channel'])
                    data2 = int(data[0])*127

                    iac.noteOn(channel,int(pitchNumber), data2)

                    if debug:
                        print "%s channel: %i data1: %s data2: %s" %\
                            ("noteOn",channel,int(pitchNumber),int(data2))

            # If not, print a message saying this address is unhandled.
            if not dealtWith and debug:
                print "Unhandled: ", addr, tags, data, client_address


class MainWindow(QtGui.QMainWindow):
    """
    The main OMB window.
    """
    def __init__(self):
        # Initialise super class
        super(MainWindow, self).__init__()


        # Load the conifguration file.
        self.configuration = None
        self.tvModel = QtGui.QStandardItemModel()
        self.refreshConfiguration()

        # Set up threads.
        self.threadLock = threading.Lock()
        self.serverThread = None

        

        # Initialise the user interface.
        self.initData()
        self.initUI()

        # Start the server.
        self.__startServer__()
        info = self.serverThread.oscServer.address()
        self.setStatus('OSC Server started on %s:%i' % (info[0], info[1]))

    def initMIDI(self):
        """

        """

    def initData(self):
        """
        Initialise the tableView data source.
        """
        
        labels = QtCore.QStringList(("OSC Address Pattern",
            "Destination", "Hostname:Port", "Device",
            "Channel", "Message", "Data 1", "Data 2"))
            
        self.tvModel.setHorizontalHeaderLabels(labels)

    def initUI(self):

        # Working from the top down.....

        # Main Window

        # Set the title.

        self.setWindowTitle('OSC-MIDI Bridge')
        self.resize(350, 200)

        self.addForward = QtGui.QAction(QtGui.QIcon('resources/add.png'),
            "Add", self)
        self.deleteForward = QtGui.QAction(QtGui.QIcon('resources/delete.png'),
            "Delete", self)
        self.startStop = QtGui.QAction(QtGui.QIcon('resources/play.png'),
            "Start", self)

        # Toolbar
        self.toolbar = self.addToolBar('Control')

        self.toolbar.addAction(self.addForward)
        self.toolbar.addAction(self.deleteForward)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.startStop)

        # Table view

        self.tv = QtGui.QTableView(self)
        #self.tv.setRootIsDecorated(False)
        self.tv.setModel(self.tvModel)
        behaviour = QtGui.QAbstractItemView.SelectRows
        self.tv.setSelectionBehavior(behaviour)

        for col in range(8):
            self.tv.resizeColumnToContents(col)

        # Layout

        self.stack = QtGui.QStackedWidget()
        self.stack.addWidget(self.tv)
        self.stack.addWidget(AddMIDIForwardWindow(self))

        self.setCentralWidget(self.stack)


        # Connections

        self.connect(self.startStop, QtCore.SIGNAL('triggered()'),
                     self.startStopServer)
        self.connect(self.addForward, QtCore.SIGNAL('triggered()'),
                     self.showAddForwardView)
        self.connect(self.deleteForward, QtCore.SIGNAL('triggered()'),
                     self.deleteForwardingRule)

        # Resize
        self.setGeometry(300,300, self.tv.width(), 300)

        #Statusbar
        self.setStatus('User interface initialised.')

    def setStatus(self, message):
        """
        Convenience for setting a message on the status bar.
        """
        self.statusBar().showMessage(message)

        if debug:
            print "Status: %s" % (message)

    def showAddForwardView(self):
        """
        Show the add forward view
        """
        self.stack.setCurrentIndex(1)

    def showMainView(self):

        self.stack.setCurrentIndex(0)

    def addForwardRule(self, rule):
        """
        Add the rule to the model.
        """
        self.setStatus('Adding rule: %s' % (rule))

        #{'toAddress': '', 'hostname': '', 'device': 3, 'message': 'channelPressure',
        #'fromAddress': '/example/osc/address', 'data1': '60', 'port': '10000', 'channel': 1, 'data2': '%oscdata'}

        # MIDI rules == 0
        # OSC rules == 1

        type = int(rule['ruleType'])
        addr = QtGui.QStandardItem(rule['fromAddress'])

        newXMLRule = None

        if type == 0:
            # MIDI
            device = QtGui.QStandardItem(rule['device'])
            channel = QtGui.QStandardItem(rule['channel'])
            message = QtGui.QStandardItem(rule['message'])
            data1 = QtGui.QStandardItem(rule['data1'])
            data2 = QtGui.QStandardItem(rule['data2'])

            # Blank out the OSC values
            completeHost = QtGui.QStandardItem("")
            destination = QtGui.QStandardItem("MIDI")


            # Put together some XML
            newXMLRule = etree.Element('forward',
                                        {'address':rule['fromAddress'],
                                        'destination':"midi",
                                        'device':str(rule['device']),
                                        'channel':str(rule['channel']),
                                        'message':str(rule['message']),
                                        'data1':str(rule['data1']),
                                        'data2':str(rule['data2'])})

            # Append the new element.
            self.configuration.getroot().append(newXMLRule)
            # Now write to the file.
            self.configuration.write('conf/conf.xml')

            self.tvModel.appendRow([addr, destination, completeHost, device,
                    channel, message, data1, data2])
            

        elif type == 1:
            # OSC

            if rule['toAddress']:
                # If a new forwarding address has been specified:
                destination = QtGui.QStandardItem(rule['toAddress'])
            else:
                # If not specified, leave the address the same.
                destination = QtGui.QStandardItem(rule['fromAddress'])


            host = "%s:%s" % (rule['hostname'], rule['port'])
            completeHost = QtGui.QStandardItem(host)

            # Blank out MIDI
            device = QtGui.QStandardItem("")
            channel = QtGui.QStandardItem("")
            message = QtGui.QStandardItem("")
            data1 = QtGui.QStandardItem("")
            data2 = QtGui.QStandardItem("")

            newDest = ''
            if rule['toAddress']:
                # If a new forwarding address has been specified:
                newDest = rule['toAddress']
            else:
                # If not specified, leave the address the same.
                newDest = rule['fromAddress']

            # Put together some XML
            newXMLRule = etree.Element('forward',
                                       {'address':rule['fromAddress'],
                                       'destination':newDest,
                                       'hostname':rule['hostname'],
                                       'port':rule['port']
                                       })

            self.configuration.getroot().append(newXMLRule)
            self.configuration.write('conf/conf.xml')

            self.tvModel.appendRow([addr, destination, completeHost, device,
                    channel, message, data1, data2])

        
        

        # Resize the treeView accordingly.
        for col in range(8):
            self.tv.resizeColumnToContents(col)



    def deleteForwardingRule(self):
        """
        Delete an existing forwarding rule.
        """
        self.setStatus("Deleting forwarding rule")

        # Get the selection.

        selectionModel = self.tv.selectionModel()
        selection = selectionModel.selection()
        indexes = selection.indexes()

        try:
            lastIndex = indexes[-1]
        except IndexError:
            # No selection made, so just return:
            # TODO: Error dialog here.
            return


        uniqueRows = []
        for index in indexes:
            if index.row() not in uniqueRows:
                uniqueRows.append(int(index.row()))

        uniqueRows.sort(reverse=True)

        #currentRowItems = []

        for row in uniqueRows:

            currentRowItems = []

            for col in range(int(self.tvModel.columnCount())):
                currentItem = str(self.tvModel.item(row, col).text())
                currentRowItems.append(currentItem)

                # We now have a list of the text values in the selected row.
                # We use this as a search term to find
                # one we should delete.
            fromAddr = currentRowItems[0]
            dest = currentRowItems[1]
            hostname = currentRowItems[2].split(':')
            device = currentRowItems[3]
            channel = currentRowItems[4]
            message = currentRowItems[5]
            data1 = currentRowItems[6]
            data2 = currentRowItems[7]

            searchString = ""

            if dest == "MIDI":
                searchString = 'forward[@address="%s"][@destination="midi"][@channel="%s"][@message="%s"][@data1="%s"][@data2="%s"]' % \
                    (fromAddr, channel, message, data1, data2)
            elif dest[0] == "/":
                # OSC
                searchString = 'forward[@address="%s"][@destination="%s"][@hostname="%s"][@port="%s"]' % \
                    (fromAddr, dest, hostname[0], hostname[1])

            print searchString

            elementsToRemove = self.configuration.findall(searchString)

            for element in elementsToRemove:
                self.configuration.getroot().remove(element)

            self.configuration.write('conf/conf.xml')


        for row in uniqueRows:
            self.tvModel.removeRow(row)
            self.setStatus('Deleted rule number %i.' %(row+1))





        

    def getDefaultIPAddress(self):
        """
        Attempts to resolve an IP address from the current hostname.

        If not possible, returns 127.0.0.1
        """
        ipAddress = socket.gethostbyname(socket.gethostname())
        defaultipAddress = self.configuration.findall('server')[0].attrib['ipAddress'] \
            or '127.0.0.1'

        # If socket.gethostbyname() fails for some reason,
        # choose loopback interface.
        # I.e localhost.
        if not ipAddress:
            ipAddress = defaultipAddress

        return ipAddress

    def getDefaultPort(self):
        """
        Get the default port from the configuration file, or return 10000 if
        fail.
        """

        port = int(self.configuration.findall('server')[0].attrib['port'])

        if port:
            return port
        else:
            return 10000

    def __startServer__(self):
        """
        Start the server.
        """
        # Check to see if there is already a thread and server running:

        if self.serverThread:
            if not self.serverThread.is_alive():
                self.serverThread = ServerThread(self.getDefaultIPAddress(),
                    self.getDefaultPort())
        else:
            self.serverThread = ServerThread(self.getDefaultIPAddress(),
                self.getDefaultPort())

        self.serverThread.start()

    def __stopServer__(self):
        """
        Stop the server.
        """
        self.serverThread.oscServer.close()
        
    def startStopServer(self):
        """
        Start/stop the server depending on it's current state.
        """
        sender = self.sender()

        if self.serverThread.oscServer.running:
            self.setStatus('Stopping OSC Server')
            self.serverThread.oscServer.close()
        else:
            self.setStatus('Starting OSC Server')
            self.__startServer__()


    def refreshConfiguration(self):
        """
        Reload the configuration file and set up the table view model.

        The table columns are in this format:

        ||OSCAddr||Dest||toOSC||name:Port||dev||chan||msg||Data 1||Data 2||
        """

        if not self.configuration:
            self.configuration = etree.parse("conf/conf.xml")

        # Turn this XML into some values
        forwards = self.configuration.findall('forward')

        for forward in forwards:
            if forward.attrib['destination'] == 'midi':
                # For MIDI forwards
                addr = QtGui.QStandardItem(forward.attrib['address'])
                destination = QtGui.QStandardItem('MIDI')
                host = QtGui.QStandardItem('')

                try:
                    device = QtGui.QStandardItem(forward.attrib['device'])
                except KeyError:
                    device = QtGui.QStandardItem('default')
                    
                channel = QtGui.QStandardItem(forward.attrib['channel'])
                message = QtGui.QStandardItem(forward.attrib['message'])
                data1 = QtGui.QStandardItem(forward.attrib['data1'])
                data2 = QtGui.QStandardItem(forward.attrib['data2'])
                
                self.tvModel.appendRow([addr, destination, host, device,
                    channel, message, data1, data2])

            if forward.attrib['destination'][0] == '/':
                # For OSC Forwards
                addr = QtGui.QStandardItem(forward.attrib['address'])
                destination = QtGui.QStandardItem(forward.attrib['destination'])
                
                hostStr = '%s:%s' % (forward.attrib['hostname'], 
                    forward.attrib['port'])
                host = QtGui.QStandardItem(hostStr)

                device = QtGui.QStandardItem('')
                channel = QtGui.QStandardItem('')
                message = QtGui.QStandardItem('')
                data1 = QtGui.QStandardItem('')
                data2 = QtGui.QStandardItem('')

                self.tvModel.appendRow([addr, destination, host, device,
                    channel, message, data1, data2])




    



if __name__ == "__main__":
    # If this module is being run directly, load the user interface
    # for testing purposes.
    import sys

    app = QtGui.QApplication(sys.argv)
    mw = MainWindow()
    mw.show()
    sys.exit(app.exec_())

